import binarySearch from '../../Core/binarySearch.js'
import ClockRange from '../../Core/ClockRange.js'
import ClockStep from '../../Core/ClockStep.js'
import defined from '../../Core/defined.js'
import DeveloperError from '../../Core/DeveloperError.js'
import JulianDate from '../../Core/JulianDate.js'
import knockout from '../../ThirdParty/knockout.js'
import createCommand from '../createCommand.js'
import ToggleButtonViewModel from '../ToggleButtonViewModel.js'

const monthNames = [
  'Jan',
  'Feb',
  'Mar',
  'Apr',
  'May',
  'Jun',
  'Jul',
  'Aug',
  'Sep',
  'Oct',
  'Nov',
  'Dec'
]
const realtimeShuttleRingAngle = 15
const maxShuttleRingAngle = 105

function numberComparator(left, right) {
  return left - right
}

function getTypicalMultiplierIndex(multiplier, shuttleRingTicks) {
  const index = binarySearch(shuttleRingTicks, multiplier, numberComparator)
  return index < 0 ? ~index : index
}

function angleToMultiplier(angle, shuttleRingTicks) {
  // Use a linear scale for -1 to 1 between -15 < angle < 15 degrees
  if (Math.abs(angle) <= realtimeShuttleRingAngle) {
    return angle / realtimeShuttleRingAngle
  }

  const minp = realtimeShuttleRingAngle
  const maxp = maxShuttleRingAngle
  let maxv
  const minv = 0
  let scale
  if (angle > 0) {
    maxv = Math.log(shuttleRingTicks[shuttleRingTicks.length - 1])
    scale = (maxv - minv) / (maxp - minp)
    return Math.exp(minv + scale * (angle - minp))
  }

  maxv = Math.log(-shuttleRingTicks[0])
  scale = (maxv - minv) / (maxp - minp)
  return -Math.exp(minv + scale * (Math.abs(angle) - minp))
}

function multiplierToAngle(multiplier, shuttleRingTicks, clockViewModel) {
  if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
    return realtimeShuttleRingAngle
  }

  if (Math.abs(multiplier) <= 1) {
    return multiplier * realtimeShuttleRingAngle
  }

  const fastedMultipler = shuttleRingTicks[shuttleRingTicks.length - 1]
  if (multiplier > fastedMultipler) {
    multiplier = fastedMultipler
  } else if (multiplier < -fastedMultipler) {
    multiplier = -fastedMultipler
  }

  const minp = realtimeShuttleRingAngle
  const maxp = maxShuttleRingAngle
  let maxv
  const minv = 0
  let scale

  if (multiplier > 0) {
    maxv = Math.log(fastedMultipler)
    scale = (maxv - minv) / (maxp - minp)
    return (Math.log(multiplier) - minv) / scale + minp
  }

  maxv = Math.log(-shuttleRingTicks[0])
  scale = (maxv - minv) / (maxp - minp)
  return -((Math.log(Math.abs(multiplier)) - minv) / scale + minp)
}

/**
 * The view model for the {@link Animation} widget.
 * @alias AnimationViewModel
 * @constructor
 *
 * @param {ClockViewModel} clockViewModel The ClockViewModel instance to use.
 *
 * @see Animation
 */
function AnimationViewModel(clockViewModel) {
  // >>includeStart('debug', pragmas.debug);
  if (!defined(clockViewModel)) {
    throw new DeveloperError('clockViewModel is required.')
  }
  // >>includeEnd('debug');

  const that = this
  this._clockViewModel = clockViewModel
  this._allShuttleRingTicks = []
  this._dateFormatter = AnimationViewModel.defaultDateFormatter
  this._timeFormatter = AnimationViewModel.defaultTimeFormatter

  /**
   * Gets or sets whether the shuttle ring is currently being dragged.  This property is observable.
   * @type {Boolean}
   * @default false
   */
  this.shuttleRingDragging = false

  /**
   * Gets or sets whether dragging the shuttle ring should cause the multiplier
   * to snap to the defined tick values rather than interpolating between them.
   * This property is observable.
   * @type {Boolean}
   * @default false
   */
  this.snapToTicks = false

  knockout.track(this, [
    '_allShuttleRingTicks',
    '_dateFormatter',
    '_timeFormatter',
    'shuttleRingDragging',
    'snapToTicks'
  ])

  this._sortedFilteredPositiveTicks = []

  this.setShuttleRingTicks(AnimationViewModel.defaultTicks)

  /**
   * Gets the string representation of the current time.  This property is observable.
   * @type {String}
   */
  this.timeLabel = undefined
  knockout.defineProperty(this, 'timeLabel', function() {
    return that._timeFormatter(that._clockViewModel.currentTime, that)
  })

  /**
   * Gets the string representation of the current date.  This property is observable.
   * @type {String}
   */
  this.dateLabel = undefined
  knockout.defineProperty(this, 'dateLabel', function() {
    return that._dateFormatter(that._clockViewModel.currentTime, that)
  })

  /**
   * Gets the string representation of the current multiplier.  This property is observable.
   * @type {String}
   */
  this.multiplierLabel = undefined
  knockout.defineProperty(this, 'multiplierLabel', function() {
    const clockViewModel = that._clockViewModel
    if (clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK) {
      return 'Today'
    }

    const multiplier = clockViewModel.multiplier

    // If it's a whole number, just return it.
    if (multiplier % 1 === 0) {
      return `${multiplier.toFixed(0)}x`
    }

    // Convert to decimal string and remove any trailing zeroes
    return `${multiplier.toFixed(3).replace(/0{0,3}$/, '')}x`
  })

  /**
   * Gets or sets the current shuttle ring angle.  This property is observable.
   * @type {Number}
   */
  this.shuttleRingAngle = undefined
  knockout.defineProperty(this, 'shuttleRingAngle', {
    get: function() {
      return multiplierToAngle(
        clockViewModel.multiplier,
        that._allShuttleRingTicks,
        clockViewModel
      )
    },
    set: function(angle) {
      angle = Math.max(
        Math.min(angle, maxShuttleRingAngle),
        -maxShuttleRingAngle
      )
      const ticks = that._allShuttleRingTicks

      const clockViewModel = that._clockViewModel
      clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK_MULTIPLIER

      // If we are at the max angle, simply return the max value in either direction.
      if (Math.abs(angle) === maxShuttleRingAngle) {
        clockViewModel.multiplier =
          angle > 0 ? ticks[ticks.length - 1] : ticks[0]
        return
      }

      let multiplier = angleToMultiplier(angle, ticks)
      if (that.snapToTicks) {
        multiplier = ticks[getTypicalMultiplierIndex(multiplier, ticks)]
      } else if (multiplier !== 0) {
        const positiveMultiplier = Math.abs(multiplier)

        if (positiveMultiplier > 100) {
          const numDigits = positiveMultiplier.toFixed(0).length - 2
          const divisor = Math.pow(10, numDigits)
          multiplier = (Math.round(multiplier / divisor) * divisor) | 0
        } else if (positiveMultiplier > realtimeShuttleRingAngle) {
          multiplier = Math.round(multiplier)
        } else if (positiveMultiplier > 1) {
          multiplier = +multiplier.toFixed(1)
        } else if (positiveMultiplier > 0) {
          multiplier = +multiplier.toFixed(2)
        }
      }
      clockViewModel.multiplier = multiplier
    }
  })

  this._canAnimate = undefined
  knockout.defineProperty(this, '_canAnimate', function() {
    const clockViewModel = that._clockViewModel
    const clockRange = clockViewModel.clockRange

    if (that.shuttleRingDragging || clockRange === ClockRange.UNBOUNDED) {
      return true
    }

    const multiplier = clockViewModel.multiplier
    const currentTime = clockViewModel.currentTime
    const startTime = clockViewModel.startTime

    let result = false
    if (clockRange === ClockRange.LOOP_STOP) {
      result =
        JulianDate.greaterThan(currentTime, startTime) ||
        (currentTime.equals(startTime) && multiplier > 0)
    } else {
      const stopTime = clockViewModel.stopTime
      result =
        (JulianDate.greaterThan(currentTime, startTime) &&
          JulianDate.lessThan(currentTime, stopTime)) || //
        (currentTime.equals(startTime) && multiplier > 0) || //
        (currentTime.equals(stopTime) && multiplier < 0)
    }

    if (!result) {
      clockViewModel.shouldAnimate = false
    }
    return result
  })

  this._isSystemTimeAvailable = undefined
  knockout.defineProperty(this, '_isSystemTimeAvailable', function() {
    const clockViewModel = that._clockViewModel
    const clockRange = clockViewModel.clockRange
    if (clockRange === ClockRange.UNBOUNDED) {
      return true
    }

    const systemTime = clockViewModel.systemTime
    return (
      JulianDate.greaterThanOrEquals(systemTime, clockViewModel.startTime) &&
      JulianDate.lessThanOrEquals(systemTime, clockViewModel.stopTime)
    )
  })

  this._isAnimating = undefined
  knockout.defineProperty(this, '_isAnimating', function() {
    return (
      that._clockViewModel.shouldAnimate &&
      (that._canAnimate || that.shuttleRingDragging)
    )
  })

  const pauseCommand = createCommand(function() {
    const clockViewModel = that._clockViewModel
    if (clockViewModel.shouldAnimate) {
      clockViewModel.shouldAnimate = false
    } else if (that._canAnimate) {
      clockViewModel.shouldAnimate = true
    }
  })

  this._pauseViewModel = new ToggleButtonViewModel(pauseCommand, {
    toggled: knockout.computed(function() {
      return !that._isAnimating
    }),
    tooltip: 'Pause'
  })

  const playReverseCommand = createCommand(function() {
    const clockViewModel = that._clockViewModel
    const multiplier = clockViewModel.multiplier
    if (multiplier > 0) {
      clockViewModel.multiplier = -multiplier
    }
    clockViewModel.shouldAnimate = true
  })

  this._playReverseViewModel = new ToggleButtonViewModel(playReverseCommand, {
    toggled: knockout.computed(function() {
      return that._isAnimating && clockViewModel.multiplier < 0
    }),
    tooltip: 'Play Reverse'
  })

  const playForwardCommand = createCommand(function() {
    const clockViewModel = that._clockViewModel
    const multiplier = clockViewModel.multiplier
    if (multiplier < 0) {
      clockViewModel.multiplier = -multiplier
    }
    clockViewModel.shouldAnimate = true
  })

  this._playForwardViewModel = new ToggleButtonViewModel(playForwardCommand, {
    toggled: knockout.computed(function() {
      return (
        that._isAnimating &&
        clockViewModel.multiplier > 0 &&
        clockViewModel.clockStep !== ClockStep.SYSTEM_CLOCK
      )
    }),
    tooltip: 'Play Forward'
  })

  const playRealtimeCommand = createCommand(function() {
    that._clockViewModel.clockStep = ClockStep.SYSTEM_CLOCK
  }, knockout.getObservable(this, '_isSystemTimeAvailable'))

  this._playRealtimeViewModel = new ToggleButtonViewModel(playRealtimeCommand, {
    toggled: knockout.computed(function() {
      return clockViewModel.clockStep === ClockStep.SYSTEM_CLOCK
    }),
    tooltip: knockout.computed(function() {
      return that._isSystemTimeAvailable
        ? 'Today (real-time)'
        : 'Current time not in range'
    })
  })

  this._slower = createCommand(function() {
    const clockViewModel = that._clockViewModel
    const shuttleRingTicks = that._allShuttleRingTicks
    const multiplier = clockViewModel.multiplier
    const index = getTypicalMultiplierIndex(multiplier, shuttleRingTicks) - 1
    if (index >= 0) {
      clockViewModel.multiplier = shuttleRingTicks[index]
    }
  })

  this._faster = createCommand(function() {
    const clockViewModel = that._clockViewModel
    const shuttleRingTicks = that._allShuttleRingTicks
    const multiplier = clockViewModel.multiplier
    const index = getTypicalMultiplierIndex(multiplier, shuttleRingTicks) + 1
    if (index < shuttleRingTicks.length) {
      clockViewModel.multiplier = shuttleRingTicks[index]
    }
  })
}

/**
 * Gets or sets the default date formatter used by new instances.
 *
 * @member
 * @type {AnimationViewModel.DateFormatter}
 */
AnimationViewModel.defaultDateFormatter = function(date, viewModel) {
  const gregorianDate = JulianDate.toGregorianDate(date)
  return `${monthNames[gregorianDate.month - 1]} ${gregorianDate.day} ${
    gregorianDate.year
  }`
}

/**
 * Gets or sets the default array of known clock multipliers associated with new instances of the shuttle ring.
 * @type {Number[]}
 */
AnimationViewModel.defaultTicks = [
  //
  0.001,
  0.002,
  0.005,
  0.01,
  0.02,
  0.05,
  0.1,
  0.25,
  0.5,
  1.0,
  2.0,
  5.0,
  10.0, //
  15.0,
  30.0,
  60.0,
  120.0,
  300.0,
  600.0,
  900.0,
  1800.0,
  3600.0,
  7200.0,
  14400.0, //
  21600.0,
  43200.0,
  86400.0,
  172800.0,
  345600.0,
  604800.0
]

/**
 * Gets or sets the default time formatter used by new instances.
 *
 * @member
 * @type {AnimationViewModel.TimeFormatter}
 */
AnimationViewModel.defaultTimeFormatter = function(date, viewModel) {
  const gregorianDate = JulianDate.toGregorianDate(date)
  const millisecond = Math.round(gregorianDate.millisecond)
  if (Math.abs(viewModel._clockViewModel.multiplier) < 1) {
    return `${gregorianDate.hour
      .toString()
      .padStart(2, '0')}:${gregorianDate.minute
      .toString()
      .padStart(2, '0')}:${gregorianDate.second
      .toString()
      .padStart(2, '0')}.${millisecond.toString().padStart(3, '0')}`
  }
  return `${gregorianDate.hour
    .toString()
    .padStart(2, '0')}:${gregorianDate.minute
    .toString()
    .padStart(2, '0')}:${gregorianDate.second.toString().padStart(2, '0')} UTC`
}

/**
 * Gets a copy of the array of positive known clock multipliers to associate with the shuttle ring.
 *
 * @returns {Number[]} The array of known clock multipliers associated with the shuttle ring.
 */
AnimationViewModel.prototype.getShuttleRingTicks = function() {
  return this._sortedFilteredPositiveTicks.slice(0)
}

/**
 * Sets the array of positive known clock multipliers to associate with the shuttle ring.
 * These values will have negative equivalents created for them and sets both the minimum
 * and maximum range of values for the shuttle ring as well as the values that are snapped
 * to when a single click is made.  The values need not be in order, as they will be sorted
 * automatically, and duplicate values will be removed.
 *
 * @param {Number[]} positiveTicks The list of known positive clock multipliers to associate with the shuttle ring.
 */
AnimationViewModel.prototype.setShuttleRingTicks = function(positiveTicks) {
  // >>includeStart('debug', pragmas.debug);
  if (!defined(positiveTicks)) {
    throw new DeveloperError('positiveTicks is required.')
  }
  // >>includeEnd('debug');

  let i
  let len
  let tick

  const hash = {}
  const sortedFilteredPositiveTicks = this._sortedFilteredPositiveTicks
  sortedFilteredPositiveTicks.length = 0
  for (i = 0, len = positiveTicks.length; i < len; ++i) {
    tick = positiveTicks[i]
    // filter duplicates
    if (!hash.hasOwnProperty(tick)) {
      hash[tick] = true
      sortedFilteredPositiveTicks.push(tick)
    }
  }
  sortedFilteredPositiveTicks.sort(numberComparator)

  const allTicks = []
  for (len = sortedFilteredPositiveTicks.length, i = len - 1; i >= 0; --i) {
    tick = sortedFilteredPositiveTicks[i]
    if (tick !== 0) {
      allTicks.push(-tick)
    }
  }
  Array.prototype.push.apply(allTicks, sortedFilteredPositiveTicks)

  this._allShuttleRingTicks = allTicks
}

Object.defineProperties(AnimationViewModel.prototype, {
  /**
   * Gets a command that decreases the speed of animation.
   * @memberof AnimationViewModel.prototype
   * @type {Command}
   */
  slower: {
    get: function() {
      return this._slower
    }
  },

  /**
   * Gets a command that increases the speed of animation.
   * @memberof AnimationViewModel.prototype
   * @type {Command}
   */
  faster: {
    get: function() {
      return this._faster
    }
  },

  /**
   * Gets the clock view model.
   * @memberof AnimationViewModel.prototype
   *
   * @type {ClockViewModel}
   */
  clockViewModel: {
    get: function() {
      return this._clockViewModel
    }
  },

  /**
   * Gets the pause toggle button view model.
   * @memberof AnimationViewModel.prototype
   *
   * @type {ToggleButtonViewModel}
   */
  pauseViewModel: {
    get: function() {
      return this._pauseViewModel
    }
  },

  /**
   * Gets the reverse toggle button view model.
   * @memberof AnimationViewModel.prototype
   *
   * @type {ToggleButtonViewModel}
   */
  playReverseViewModel: {
    get: function() {
      return this._playReverseViewModel
    }
  },

  /**
   * Gets the play toggle button view model.
   * @memberof AnimationViewModel.prototype
   *
   * @type {ToggleButtonViewModel}
   */
  playForwardViewModel: {
    get: function() {
      return this._playForwardViewModel
    }
  },

  /**
   * Gets the realtime toggle button view model.
   * @memberof AnimationViewModel.prototype
   *
   * @type {ToggleButtonViewModel}
   */
  playRealtimeViewModel: {
    get: function() {
      return this._playRealtimeViewModel
    }
  },

  /**
   * Gets or sets the function which formats a date for display.
   * @memberof AnimationViewModel.prototype
   *
   * @type {AnimationViewModel.DateFormatter}
   * @default AnimationViewModel.defaultDateFormatter
   */
  dateFormatter: {
    // TODO:@exception {DeveloperError} dateFormatter must be a function.
    get: function() {
      return this._dateFormatter
    },
    set: function(dateFormatter) {
      // >>includeStart('debug', pragmas.debug);
      if (typeof dateFormatter !== 'function') {
        throw new DeveloperError('dateFormatter must be a function')
      }
      // >>includeEnd('debug');

      this._dateFormatter = dateFormatter
    }
  },

  /**
   * Gets or sets the function which formats a time for display.
   * @memberof AnimationViewModel.prototype
   *
   * @type {AnimationViewModel.TimeFormatter}
   * @default AnimationViewModel.defaultTimeFormatter
   */
  timeFormatter: {
    // TODO:@exception {DeveloperError} timeFormatter must be a function.
    get: function() {
      return this._timeFormatter
    },
    set: function(timeFormatter) {
      // >>includeStart('debug', pragmas.debug);
      if (typeof timeFormatter !== 'function') {
        throw new DeveloperError('timeFormatter must be a function')
      }
      // >>includeEnd('debug');

      this._timeFormatter = timeFormatter
    }
  }
})

// Currently exposed for tests.
AnimationViewModel._maxShuttleRingAngle = maxShuttleRingAngle
AnimationViewModel._realtimeShuttleRingAngle = realtimeShuttleRingAngle

/**
 * A function that formats a date for display.
 * @callback AnimationViewModel.DateFormatter
 *
 * @param {JulianDate} date The date to be formatted
 * @param {AnimationViewModel} viewModel The AnimationViewModel instance requesting formatting.
 * @returns {String} The string representation of the calendar date portion of the provided date.
 */

/**
 * A function that formats a time for display.
 * @callback AnimationViewModel.TimeFormatter
 *
 * @param {JulianDate} date The date to be formatted
 * @param {AnimationViewModel} viewModel The AnimationViewModel instance requesting formatting.
 * @returns {String} The string representation of the time portion of the provided date.
 */
export default AnimationViewModel
